{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# \"Visualize\" Building Numbers (because we can!)\n",
    "\n",
    "This notebook uses the free Microsoft \"US Builing Footprints\" [dataset](https://github.com/Microsoft/USBuildingFootprints), enriched with [HERE](https://here.com) geolocation information and loaded from a public HERE [Data Hub](https://here.xyz) \"space\". It searches footprints (GeoJSON features) up to 2 km away from any selected location and colors them according to their house numbers given color scale for all numbers in this area. While the practical use is limited a certain grid structure can be observed that will likely look less rigid outside the US. Something very similar can be done to show building footprint area _sizes_ for example.\n",
    "\n",
    "This code uses a new Python package named \"xyzspaces\" (see [GitHub repo](https://github.com/heremaps/xyz-spaces-python)) providing access to the Open Source XYZ technology at the core of the Data Hub installation at HERE. A registration to the HERE [Data Hub](https://here.xyz) and basic use is free up to a certain limit, but some features require a \"pro\" plan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import re\n",
    "from functools import partial\n",
    "from io import StringIO\n",
    "\n",
    "import geojson\n",
    "import bqplot.pyplot as plt\n",
    "from branca.colormap import LinearColormap\n",
    "from ipywidgets import Button, HTML, Layout\n",
    "from ipyleaflet import (basemaps, FullScreenControl, GeoJSON, LayersControl,\n",
    "    LegendControl, Map, Marker, Popup, SearchControl, ScaleControl, WidgetControl)\n",
    "\n",
    "import xyzspaces\n",
    "# The following needs an \"XYZ_TOKEN\" environment variable:\n",
    "from xyzspaces.datasets import get_microsoft_buildings_space as msbf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def house_color(feat, cs):\n",
    "    try:\n",
    "        return cs(int(feat[\"properties\"][\"estimatedHouseNumber\"]))\n",
    "    except:\n",
    "        return \"#aaaaaa\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def hist(data):\n",
    "    fig = plt.figure(fig_margin=dict(top=5, bottom=35, left=30, right=15))\n",
    "    hist = plt.hist(data)\n",
    "    fig.layout.width = \"200px\"\n",
    "    fig.layout.height = \"150px\"\n",
    "    hist.bins = 20\n",
    "    if data:\n",
    "        vmin, vmax = min(data), max(data)\n",
    "        cs = LinearColormap([\"green\", \"blue\"], vmin=vmin, vmax=vmax)\n",
    "        hist.colors = [cs(vmin + i * (vmax - vmin)/hist.bins) for i in range(hist.bins)]\n",
    "    vx, hx = fig.axes\n",
    "    vx.num_ticks = 4\n",
    "    hx.num_ticks = 5\n",
    "    hx.label = \"Histogram Bldg. Numbers\"\n",
    "    return fig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def marker_moved(event, mymap=None, mylegend=None, myhist=None):\n",
    "    if event[\"name\"] not in [\"location\", \"visible\"]:\n",
    "        return\n",
    "    mymap.layers = tuple([l for l in mymap.layers if type(l) != GeoJSON])\n",
    "    lat, lon = event[\"new\"] if event[\"name\"] == \"location\" else event[\"owner\"].location\n",
    "    features = list(msbf().spatial_search(lat=lat, lon=lon, radius=2000))\n",
    "    house_nums = [int(feat[\"properties\"][\"estimatedHouseNumber\"]) \n",
    "        for feat in features if \"estimatedHouseNumber\" in feat[\"properties\"] \\\n",
    "             and re.match(\"^\\d+$\", feat[\"properties\"][\"estimatedHouseNumber\"])]\n",
    "    vmin, vmax = min(house_nums), max(house_nums)\n",
    "    cs = LinearColormap([\"green\", \"blue\"], vmin=vmin, vmax=vmax)\n",
    "\n",
    "    data = geojson.FeatureCollection(features=features)\n",
    "    gj = GeoJSON(data=data, name=\"Building numbers\",\n",
    "        hover_style={\"weight\": 4, 'fillOpacity': 0.6},\n",
    "        style_callback=lambda feat: {\"weight\": 2, \"color\": house_color(feat, cs)})\n",
    "    mymap += gj\n",
    "\n",
    "    myhist.widget = hist(house_nums)\n",
    "    if myhist not in mymap.controls:\n",
    "        mymap += myhist\n",
    "\n",
    "    mylegend.legend = {f\"Max: {vmax}\": \"blue\", f\"Min: {vmin}\": \"green\", \"Unknown\": \"#aaaaaa\"}\n",
    "    if mylegend not in mymap.controls:\n",
    "        mymap += mylegend\n",
    "    event[\"owner\"].popup = HTML(\n",
    "        f\"<b>Lat/Lon:</b> {lat}/{lon},<br/><b>Radius:</b> 2000 m, <b>#Features:</b> {len(features)}</center>\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_marker(button, mymap=None, mylegend=None, myhist=None):\n",
    "    mymap.layers = tuple([l for l in mymap.layers if type(l) not in [Marker, GeoJSON]])\n",
    "    mk = Marker(location=mymap.center, name=\"Center\", visible=False)\n",
    "    mk.observe(partial(marker_moved, mymap=mymap, mylegend=mylegend, myhist=myhist))\n",
    "    mymap += mk\n",
    "    mk.visible = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lat, lon = 38.89759, -77.03665\n",
    "m = Map(center=[lat, lon], zoom=14, basemap=basemaps.CartoDB.Positron)\n",
    "m.layout.width = \"1000px\"\n",
    "m.layout.height = \"600px\"\n",
    "m += FullScreenControl(position=\"topleft\")\n",
    "m += LayersControl(position=\"topleft\")\n",
    "m += ScaleControl(position=\"bottomleft\")\n",
    "legend = {\"Max\": \"blue\", \"Min\": \"green\", \"Unknown\": \"#aaaaaa\"}\n",
    "lg = LegendControl(legend=legend, name=\"Bldg. numbers\", position=\"topright\")\n",
    "\n",
    "hfig = HTML()\n",
    "hi = WidgetControl(name=\"Histogramm\", widget=hfig, position=\"bottomright\")\n",
    "\n",
    "btn = Button(icon=\"plus-circle\", tooltip=\"Add at center\", layout=Layout(width=\"30px\"))\n",
    "btn.on_click(partial(add_marker, mymap=m, mylegend=lg, myhist=hi))\n",
    "m += WidgetControl(widget=btn, position=\"topleft\")\n",
    "\n",
    "m += SearchControl(auto_type=True, auto_collapse=True, position=\"topleft\",\n",
    "    url='https://nominatim.openstreetmap.org/search?format=json&q={s}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
